package org.dding.mvc.servlet;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.RequestContext;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.servlet.ServletRequestContext;
import org.dding.core.util.StringUtils;
import org.dding.mvc.FileUploaded;
import org.dding.mvc.InvocationContext;
import org.dding.mvc.PageContext;
import org.dding.mvc.SessionContext;
import org.dding.mvc.WebContextLoader;
import org.dding.mvc.annotation.ActionMethod;

/**
 * 控制器Servlet，所有的Servlet请求，均由该Servlet负责分发
 * <p/>
 * ControllerServlet缓存有所有Action，会根据 URL 规则交给正确的 Action 执行，
 * Action则为一个纯粹的Java类，不依赖于Web容器
 * <p/>
 * ControllerServlet 使用 forward("/WEB-INF/xxx.html") 将请求发送到 /WEB-INF 中
 * 
 */
public class ControllerServlet extends HttpServlet {

	private static final long serialVersionUID = 5169508658451941198L;
	public static final String PAGE_CONTEXT = "__PAGE_CONTEXT__";
	public static final String INVOCATION_CONTEXT = "__INVOCATION_CONTEXT__";
	public static final String MAX_UPLOAD_FILE_SIZE_KEY = "MAX_UPLOAD_FILE_SIZE";
	public static final String VIEW_DIR_KEY = "VIEW_DIR";
	public static final String ACTION_SUFFIX_KEY = "ACTION_SUFFIX";
	public static final String DEFAULT_ENCODING_KEY = "DEFAULT_ENCODING";

	public static final String MULTIPART = "multipart/";

	public static String DEFAULT_ENCODING = "UTF-8";

	static String ACTION_SUFFIX = ".do";
	static String VIEW_DIR = "views";
	static int MAX_UPLOAD_FILE_SIZE = 5 * 1000 * 1000;

	public void init(ServletConfig servletConfig) throws ServletException {
		super.init(servletConfig);
		String actionSuffix = servletConfig.getInitParameter(ACTION_SUFFIX_KEY);
		if (actionSuffix != null && actionSuffix.trim().length() != 0) {
			ACTION_SUFFIX = actionSuffix;
		}
		// default encoding
		String defaultEncoding = servletConfig
				.getInitParameter(DEFAULT_ENCODING_KEY);
		if (defaultEncoding != null && defaultEncoding.trim().length() != 0) {
			DEFAULT_ENCODING = defaultEncoding;
		}

		// view dir
		String viewDir = servletConfig.getInitParameter(VIEW_DIR_KEY);
		if (viewDir != null && viewDir.trim().length() != 0) {
			VIEW_DIR = viewDir;
		}

		// max upload limit
		String maxUploadFileSize = servletConfig.getServletContext()
				.getInitParameter(MAX_UPLOAD_FILE_SIZE_KEY);
		if (maxUploadFileSize != null && maxUploadFileSize.trim().length() != 0) {
			MAX_UPLOAD_FILE_SIZE = Integer.parseInt(maxUploadFileSize);
		}

	}

	public static String getActionSuffix() {
		return ACTION_SUFFIX;
	}

	public static String getViewDir() {
		return VIEW_DIR;
	}

	protected void service(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		request.setCharacterEncoding(DEFAULT_ENCODING);
		String pathInfo = request.getPathInfo();
		if (pathInfo.endsWith(ACTION_SUFFIX)) {
			// action
			forwardAction(request, response);
		} else {
			forwardView(request, response);
		}
	}

	protected void forwardView(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		String pathInfo = request.getPathInfo();
		int slashIndex = pathInfo.indexOf("/", 2);
		String serviceName = pathInfo.substring(1, slashIndex);
		if (!WebContextLoader.isServiceExist(serviceName)) {
			throw new ServletException("Invalid request url: "
					+ request.getRequestURL());
		}

		String filePath = pathInfo.substring(slashIndex);
		String realPath = WebContextLoader.getServicePath(serviceName)
				+ File.separator + VIEW_DIR + filePath;
		request.getRequestDispatcher(realPath).forward(request, response);
	}

	protected void forwardAction(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		String pathInfo = request.getPathInfo();
		String queryString = request.getQueryString();
		int slashIndex = pathInfo.indexOf("/", 2);
		if (slashIndex < 0) {
			throw new ServletException("Invalid request url:" + pathInfo
					+ ",not contain action name or method");
		}
		String actionName = pathInfo.substring(1, slashIndex);
		if (!WebContextLoader.isServiceExist(actionName)) {
			throw new ServletException("Invalid action:" + actionName
					+ ",because of not exist.");
		}

		int lastSlashIndex = pathInfo.lastIndexOf("/");
		int actionMethodDotIndex = pathInfo.indexOf(ACTION_SUFFIX,
				lastSlashIndex);
		String actionMethodName = pathInfo.substring(lastSlashIndex + 1,
				actionMethodDotIndex);

		// 会导致取出的值为数组问题，所以只能使用下面的循环
		final Map<String, String[]> parameterMap = new HashMap<String, String[]>();
		final Map<String, FileUploaded> fileUploadedMap = new HashMap<String, FileUploaded>();
		if (!isMultipartContent(request)) {
			for (Enumeration enu = request.getParameterNames(); enu
					.hasMoreElements();) {
				String key = (String) enu.nextElement();
				String[] values = request.getParameterValues(key);
				if (queryString != null && queryString.contains(key)) { // 是 URL
																		// encoding
					values = decodeQueryStringParameterValues(values);
				}
				parameterMap.put(key, values);
			}
		} else { // 有文件上传
					// 从 HTTP servlet 获取 fileupload 组件需要的内容
			RequestContext requestContext = new ServletRequestContext(request);
			// 判断是否包含 multipart 内容
			if (ServletFileUpload.isMultipartContent(requestContext)) {
				// 创建基于磁盘的文件工厂
				DiskFileItemFactory factory = new DiskFileItemFactory();
				// 设置直接存储文件的极限大小，一旦超过则写入临时文件以节约内存。默认为 1024 字节
				factory.setSizeThreshold(MAX_UPLOAD_FILE_SIZE);
				// 创建上传处理器，可以处理从单个 HTML 上传的多个上传文件。
				ServletFileUpload upload = new ServletFileUpload(factory);
				// 最大允许上传的文件大小 5M
				upload.setSizeMax(MAX_UPLOAD_FILE_SIZE);
				upload.setHeaderEncoding(DEFAULT_ENCODING);
				try {
					// 处理上传
					List items = upload.parseRequest(requestContext);
					// 由于提交了表单字段信息，需要进行循环区分。
					for (Object item : items) {
						FileItem fileItem = (FileItem) item;
						if (fileItem.isFormField()) {
							// 表单内容
							parameterMap.put(fileItem.getFieldName(),
									new String[] { fileItem
											.getString(DEFAULT_ENCODING) });
						} else {
							// 如果不是表单内容，取出 multipart。
							// 上传文件路径和文件、扩展名。
							String sourcePath = fileItem.getName();
							// 获取真实文件名
							String fileName = new File(sourcePath).getName();
							// 读到内存成 FileUpload 对象
							FileUploaded fileUploaded = new FileUploaded(
									fileName, fileItem.get());
							// invocationContext.addFileUploaded(fileItem.getFieldName(),
							// fileUploaded);
							fileUploadedMap.put(fileItem.getFieldName(),
									fileUploaded);
						}
					}
				} catch (FileUploadException e) {
					throw new ServletException("File upload failed!", e);
				}
			}
		}

		try {
			// 初始化 SessionContext，并绑定到线程
			SessionContext.init(request);
			InvocationContext invocationContext = new InvocationContext(
					getServletConfig(), request, parameterMap, fileUploadedMap,
					actionName, actionMethodName, "POST".equals(request
							.getMethod().toUpperCase()));
			WebContextLoader.invokeAction(actionName, actionMethodName,
					invocationContext);
			request.setAttribute(INVOCATION_CONTEXT, invocationContext);
			// 根据 PageContext.getTargetMethod 要决定 forward 还是 redirect
			String serviceName = invocationContext.getServiceName();

			String dest = invocationContext.getPageContext().getTargeView();
			if (!StringUtils.isEmpty(serviceName)) {
				String servletPah = request.getServletPath();
				dest = servletPah + "/" + serviceName + "/" + dest;
			}

			PageContext pageContext = invocationContext.getPageContext();
			for (Entry<String, Object> entry : pageContext.getResultMap()
					.entrySet()) {
				request.setAttribute(entry.getKey(), entry.getValue());
			}

			if (invocationContext.getPageContext().getTargetMethod()
					.equals(ActionMethod.ForwardMethod.REDIRECT)) {
				response.sendRedirect(dest);
			} else if (invocationContext.getPageContext().getTargetMethod()
					.equals(ActionMethod.ForwardMethod.FLUSH)) {
				String content = (String) invocationContext
						.getResponse(InvocationContext.RESPONSE_CONTENT);
				@SuppressWarnings({ "unchecked" })
				Map<String, String> headers = (Map<String, String>) invocationContext
						.getResponse(InvocationContext.RESPONSE_HEADER);
				if (headers != null)
					for (Entry<String, String> entry : headers.entrySet()) {
						response.setHeader(entry.getKey(), entry.getValue());
					}
				response.getWriter().write(content);
				response.getWriter().flush();
			} else {
				request.getRequestDispatcher(dest).forward(request, response);
			}
		} catch (ServletException e) {
			throw e;
		} catch (Exception e) {
			throw new ServletException(e);
		} finally {
			// 解除 SessionContext 和 线程的绑定
			SessionContext.disassociateThreadSessionContext();
		}
	}

	public static boolean isMultipartContent(HttpServletRequest req) {
		if (!"POST".equals(req.getMethod().toUpperCase())) {
			return false;
		}
		String contentType = req.getContentType();
		return contentType != null
				&& contentType.toLowerCase().startsWith(MULTIPART);
	}

	public String[] decodeQueryStringParameterValues(String[] values) {
		if (values != null && values.length > 0) {
			String[] decodedValues = new String[values.length];
			for (int i = 0; i < values.length; i++) {
				try {
					decodedValues[i] = new String(
							values[i].getBytes("ISO-8859-1"), "UTF-8");
				} catch (UnsupportedEncodingException e) {
					e.printStackTrace();
					decodedValues[i] = values[i];
				}
			}
			return decodedValues;
		} else {
			return new String[0];
		}
	}
}
